refactor(cohort): one port over the list, with the vendor behind contact - #1172
Merged
Conversation
CohortPort and TargetStrategy abstracted the same Brevo list, each with one production implementation delegating to the same ContactListAdapter. The richer of the two survives; the other, its registry, its Brevo adapter and its mock are gone. BrevoTargetStrategy no longer injects the generated ContactsApi: folder and list paging moved into BrevoListAdapter and is published through contact :: api, so cohort reaches Brevo only through the wrapper and the pinned ADR-019 violation in VendorClientArchitectureTest is deleted rather than edited.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1169.
Why
cohortreached one Brevo list through two ports.CohortPort(withCohortPortRegistry,BrevoCohortAdapter,MockCohortPort) andTargetStrategy(withTargetStrategies,BrevoTargetStrategy,MockTargetStrategy) abstracted the same operations on the same list,each had exactly one production implementation, both delegated to the same
ContactListAdapter,and their member types were structurally identical. Two registries dispatched on
TargetSystem,whose only cohort-valid value is
BREVO.BrevoTargetStrategyalso injected the generatedContactsApifor folder and list paging, whichAPI ADR-019 forbids and which #1158 pinned in
VendorClientArchitectureTestat six reaches.What this achieves
One port over the external target, one test double behind it, and no vendor type reachable from
cohort— the pinned entry is deleted, and the rule now passes withPINNEDempty.How
TargetStrategysurvives, unchanged in shape apart from one addition:handle(externalId)answers with a target known only by its id, which is all the member, move and delete calls key on.
The two
CohortPortRegistryconsumers —CohortMembershipSyncServiceandCohortRemediationService— now holdTargetStrategiesand write through that.CohortPortRegistry.requirefailed terminally (NonRetryableJobException) whereTargetStrategies.requireanswers a request with a 400, and both semantics are wanted: a jobcannot retry a bean into existence, and a controller should not answer 500 for an unknown system.
So
TargetStrategiesgainedrequireForJob, which the two job-path services call;requireisunchanged for the web paths.
Deleted:
CohortPort,CohortPortRegistry,BrevoCohortAdapter,MockCohortPortandBrevoCohortAdapterTest.MockTargetStrategyabsorbed the mock's transaction-boundary recordingand
seedMember/clear, so the ITs that used it keep asserting exactly what they asserted.The catalog moved into
contact.ContactListAdaptergainedlistAll(): List<ContactListRef>and
BrevoListAdapternow pages both folders and lists, wrapping a rate-limited page the way itwraps every other failed fetch.
BrevoTargetStrategycomposes the two — folder names once, listsonce — and imports nothing from
net.blueshell.clients.listFoldersused to read a single pageof 50 and silently drop the rest; it now pages like the catalogue does.
The id shape:
Stringwins at the port,Longstays insidecontact. The reasonCohortPortgave for
Stringis the right one — a Discord snowflake and a Google group address have to sitbeside Brevo's numeric list id — while
ContactListAdapterspeaks to systems that really do key bynumber. That leaves exactly one conversion,
BrevoTargetStrategy.toBrevoId, at the edge that knowsit is talking to Brevo; the second copy went with
BrevoCohortAdapter.Not in scope
ContactAdapterSyncTarget.pushstill doescurrentExternalId!!.toLong()in a generic base class,so a non-numeric contact system would break the shared class rather than its own adapter. It is on
the contact-sync path rather than the cohort list path, and moving it means changing
ContactAdapter'sid type across
sync— a separate change, and reported rather than smuggled in here.Worth a reviewer's attention
handleis an interface default method, so a Mockito mock ofTargetStrategyreturnsnullfromit unless stubbed. That is why
CohortTargetingService.deleteTargetstill builds its bareExternalTargetby hand: routing it throughhandlebrokeCohortTargetingServiceTeston thedouble rather than on the behaviour, and the test was left alone.
MockCohortPortforMockTargetStrategy,CohortRemediationServiceTest'sRecordingCohortPortbecomes a
RecordingTargetStrategy, andCohortMembershipSyncServiceTeststubs aTargetStrategyinstead of a
CohortPort. No assertion changed meaning; the one message assertion follows therenamed exception text. Every other cohort test passes unchanged.
BrevoTargetStrategyTest's paging and rate-limit cases moved toBrevoListAdapterTest, since thepaging did. What stayed in the cohort test is the mapping and filtering that is still cohort's.
Verification
./gradlew :services:api:test— 901 passed, exit 0 (includesVendorClientArchitectureTestwithPINNEDempty, and the Modulith detection test)../gradlew :services:api:integrationTest— 1264 passed, exit 0, against a throwawaymariadb:11on port 3407 (3306 was taken), withblueshell-testandblueshellcreated.Diff breakdown —
█added░removed, scaled to the largest row.